package main

import (
	"archive/tar"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
)

// Thanks https://github.com/mholt/archiver

// compress tar.gz

// FileInfo is an os.FileInfo but optionally with
// a custom name, useful if dealing with files that
// are not actual files on disk, or which have a
// different name in an archive than on disk.
type FileInfo struct {
	os.FileInfo
	CustomName string
}

// File provides methods for accessing information about
// or contents of a file within an archive.
type File struct {
	FileInfo

	// The original header info; depends on
	// type of archive -- could be nil, too.
	Header interface{}

	// Allow the file contents to be read (and closed)
	io.ReadCloser
}

// Tar todo
type Tar struct {
	tw *tar.Writer
}

func isSymlink(fi os.FileInfo) bool {
	return fi.Mode()&os.ModeSymlink != 0
}

func (c *Tar) Write(f File) error {
	if c.tw == nil {
		return errors.New("tar archive was not created for writing first")
	}
	if f.FileInfo.Name() == "" {
		return errors.New("missing file name")
	}
	var linkTarget string
	if isSymlink(f) {
		var err error
		linkTarget, err = os.Readlink(f.Name())
		if err != nil {
			return fmt.Errorf("%s: readlink: %v", f.Name(), err)
		}
	}

	hdr, err := tar.FileInfoHeader(f, filepath.ToSlash(linkTarget))
	if err != nil {
		return fmt.Errorf("%s: making header: %v", f.Name(), err)
	}
	hdr.Name = f.CustomName
	err = c.tw.WriteHeader(hdr)
	if err != nil {
		return fmt.Errorf("%s: writing header: %v", hdr.Name, err)
	}
	if f.IsDir() {
		return nil // directories have no contents
	}

	if hdr.Typeflag == tar.TypeReg {
		if f.ReadCloser == nil {
			return fmt.Errorf("%s: no way to read file contents", f.Name())
		}
		_, err := io.Copy(c.tw, f)
		if err != nil {
			return fmt.Errorf("%s: copying contents: %v", f.Name(), err)
		}
	}
	return nil
}

// AddFile todo
func (c *Tar) AddFile(src, nameInArchive string) error {
	st, err := os.Stat(src)
	if err != nil {
		return fmt.Errorf("%s: stat: %v", src, err)
	}
	var file io.ReadCloser
	if st.Mode().IsRegular() {
		file, err = os.Open(src)
		if err != nil {
			return fmt.Errorf("%s: opening: %v", src, err)
		}
		defer file.Close()
	}
	err = c.Write(File{
		FileInfo: FileInfo{
			FileInfo:   st,
			CustomName: filepath.ToSlash(nameInArchive),
		},
		ReadCloser: file,
	})
	if err != nil {
		return fmt.Errorf("%s: writing: %s", src, err)
	}
	return nil
}

// Compressor todo
type Compressor struct {
	tar *Tar
	gzw *gzip.Writer
	fd  *os.File
}

// NewCompressor todo
func NewCompressor(out string) (*Compressor, error) {
	c := &Compressor{}
	var err error
	if c.fd, err = os.Create(out); err != nil {
		return nil, err
	}
	c.tar = &Tar{}
	c.gzw = gzip.NewWriter(c.fd)
	c.tar.tw = tar.NewWriter(c.gzw)
	return c, nil
}

// NewCompressorSTGZ todo
func NewCompressorSTGZ(out string) (*Compressor, error) {
	c := &Compressor{}
	var err error
	if c.fd, err = os.OpenFile(out, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755); err != nil {
		return nil, err
	}
	if _, err := c.fd.WriteString(STGZ); err != nil {
		c.fd.Close()
		c.fd = nil
		return nil, err
	}
	c.tar = &Tar{}
	c.gzw = gzip.NewWriter(c.fd)
	c.tar.tw = tar.NewWriter(c.gzw)
	return c, nil
}

// Close todo
func (c *Compressor) Close() error {
	if c.tar.tw != nil {
		c.tar.tw.Close()
	}
	if c.gzw != nil {
		_ = c.gzw.Close()
	}
	if c.fd != nil {
		_ = c.fd.Close()
	}
	return nil
}
